RustアプリのコンテナイメージをLambdaで動かしてみた #reinvent
現在開催中の re:Invent 2020 にてLambdaのコンテナサポートが発表されました。
【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent
AWS提供のベースイメージに関するドキュメントを確認しましたが、残念ながら現時点でRust専用のイメージは提供されていません。しかし、 Base images for custom runtimes のセクションに provided
と provided.al2
というカスタムランタイムで実装するときによく見る名前が出てきました。
Lambda向けのコンテナイメージは Lambda Runtime Interface に対応してね、と説明されていますがこれもカスタムランタイムでよく見かけるものです。Rustの場合は awslabs/aws-lambda-rust-runtime を利用すると裏側でLambda Runtime Interface経由でやり取りしてくれます。
これカスタムランタイムと同じやり方で動くんじゃない?と思い試してみたところばっちり動いてくれました。以下に試した手順を紹介します。
環境
- Rust: 1.48.0
- Docker: 20.10.0-rc1
試してみる
provided.al2
ベースのイメージで動作するように、以下の手順で試してみます。
- Lambdaアプリケーションを実装する
- Lambdaコンテナ用のイメージを作る
- AWS環境にデプロイする
Lambdaアプリケーションを実装する
Lambda以外の環境に持っていきやすくするため src/lib.rs
にメインの処理を記述し、 src/bin/hello-container.rs
にコンテナイメージ用のmain関数を実装していきます。
まずプロジェクトを作成し、 hello-container
ディレクトリに移動します。なお以降の作業はすべて同ディレクトリで行います。
$ cargo new hello-container --lib Created library `hello-container` package $ cd hello-container
次に依存クレート・実行ファイルの指定を追加します。
[package] name = "hello-container" version = "0.1.0" authors = ["yoshihitoh"] edition = "2018" # 実行ファイルの配置場所を変更しているため明示的に指定する [[bin]] name = "hello-container" path = "src/bin/hello-container.rs" [dependencies] lambda_runtime = "0.2.1" serde = { version = "1", features = ["derive"] }
イベントと出力データを定義し、ハンドラーの処理を実装します。
use serde::{Serialize, Deserialize}; #[derive(Debug, Deserialize)] pub struct HelloEvent { name: String, } #[derive(Debug, Serialize)] pub struct HelloOutput { message: String, } pub fn hello(event: HelloEvent) -> HelloOutput { HelloOutput { message: format!("Hello, {}!", event.name), } }
Lambda用の実行ファイルを実装します。
use std::error::Error as StdError; use lambda_runtime::{error::HandlerError, lambda, Context}; use hello_container::{hello, HelloEvent, HelloOutput}; fn handler(event: HelloEvent, _context: Context) -> Result<HelloOutput, HandlerError> { Ok(hello(event)) } fn main() -> Result<(), Box<dyn StdError>> { lambda!(handler); Ok(()) }
アプリケーションの実装は以上で完了です。ちゃんとコンパイルできるか cargo check
で確認します。
$ cargo check Compiling autocfg v1.0.1 Compiling libc v0.2.80 Checking cfg-if v0.1.10 Checking futures v0.1.30 Compiling semver-parser v0.7.0 Checking hello-container v0.1.0 (/Users/yoshihitoh/workspace/projects/blog/rust/lambda-container/hello-container) Finished dev [unoptimized + debuginfo] target(s) in 40.12s ...
ちゃんとコンパイルできそうですね!
Lambdaコンテナ用のイメージを作る
provided.al2
環境で動作するようにビルドします。ビルドターゲットを x86_64-unknown-linux-musl
にすればmacOS環境でもビルドできそうですが、せっかくなので provided.al2
環境でビルドしてみます。なお、Rustアプリケーションはビルド時にRustのツールチェーンが必要ですが、実行時には不要です。そこで、ビルド用・ランタイム用で分けてイメージを作ってみます。
- Dockerfile.build : ビルド用のDockerfile
- Dockerfile : ランタイム用のDockerfile
ビルド用のイメージ
provided:al2
をベースにビルド環境を構築します。
FROM public.ecr.aws/lambda/provided:al2 # リンカーとしてgccを利用する RUN yum install -y gcc # rustupでRustツールチェーンをインストールする RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable ENV PATH $PATH:/root/.cargo/bin RUN rustup install stable # ビルド対象のソースツリーをマウントする VOLUME /code # ローカル環境にRustを導入している場合は以下をコメントアウトするとビルドが早くなります #VOLUME /root/.cargo/registry #VOLUME /root/.cargo/git WORKDIR /code # provided:al2 はランタイム用の設定になっているので、ENTRYPOINTをビルド用に書き換える ENTRYPOINT ["cargo", "build", "--release"]
イメージをビルドします。
$ docker image build -t "hello-container-build" -f Dockerfile.build . Sending build context to Docker daemon 478.9MB Step 1/10 : FROM public.ecr.aws/lambda/provided:al2 ---> 759805681d04 Step 2/10 : RUN yum install -y gcc ---> Running in a9bd79498d48 Loaded plugins: ovl Resolving Dependencies --> Running transaction check ---> Package gcc.x86_64 0:7.3.1-9.amzn2 will be installed --> Processing Dependency: libgomp = 7.3.1-9.amzn2 for package: gcc-7.3.1-9.amzn2.x86_64 --> Processing Dependency: cpp = 7.3.1-9.amzn2 for package: gcc-7.3.1-9.amzn2.x86_64 ... ... (中略) ... Step 10/10 : ENTRYPOINT ["cargo", "build", "--release"] ---> Running in 7797bdb36274 Removing intermediate container 7797bdb36274 ---> b92b1f199ecd Successfully built b92b1f199ecd Successfully tagged hello-container-build:latest
ビルドが成功したらコンテナを立ち上げて、アプリケーションを provided:al2
向けにビルドします。
$ docker container run --rm \ -v $PWD:/code \ -v $HOME/.cargo/registry:/root/.cargo/registry \ -v $HOME/.cargo/git:/root/.cargo/git \ hello-container-build Compiling autocfg v1.0.1 Compiling libc v0.2.80 Compiling cfg-if v0.1.10 Compiling futures v0.1.30 Compiling semver-parser v0.7.0 ... ... (中略) ... Compiling lambda_runtime_errors v0.1.1 Compiling lambda_runtime_client v0.2.2 Compiling lambda_runtime v0.2.1 Compiling hello-container v0.1.0 (/code) Finished release [optimized] target(s) in 7m 50s
ビルドが成功すると、ローカル環境の hello-container/target/release/hello-container
に実行ファイルが出力されます。これを使ってランタイム用のイメージを作成していきます。
ランタイム用のイメージ
ランタイム用のイメージは簡単で、ビルド済みの実行ファイルをコピーしてハンドラを指定するだけです。カスタムランタイム同様ハンドラは利用しないため、適当な文字列を指定します。アプリケーション内で正しいハンドラ名が必要になる場合はその名称を指定してください。
FROM public.ecr.aws/lambda/provided:al2 # 実行ファイルを起動するようにするため、ファイル名を "bootstrap" に変更する COPY ./target/release/hello-container ${LAMBDA_RUNTIME_DIR}/bootstrap # カスタムランタイム同様ハンドラ名は利用しないため、適当な文字列を指定する。 CMD [ "lambda-handler" ]
ランタイム用のイメージをビルドします。
$ docker image build -t "hello-container" . Sending build context to Docker daemon 261.4MB Step 1/3 : FROM public.ecr.aws/lambda/provided:al2 ---> 759805681d04 Step 2/3 : COPY ./target/release/hello-container ${LAMBDA_RUNTIME_DIR}/bootstrap ---> 7e36acd891d5 Step 3/3 : CMD [ "dummy-handler" ] ---> Running in a129ddbc7c39 Removing intermediate container a129ddbc7c39 ---> 7657889181e4 Successfully built 7657889181e4 Successfully tagged hello-container:latest
ちゃんとビルドできましたね!以上で準備完了です。
AWS環境にデプロイする
ECRにコンテナイメージをプッシュしてLambdaで動かしてみます。まず、ECRにリポジトリを作成します。
ECRにランタイム用のコンテナイメージをプッシュする
$ aws ecr create-repository --repository-name hello-container { "repository": { "repositoryArn": "arn:aws:ecr:ap-northeast-1:xxxxxxxxxxxx:repository/hello-container", "registryId": "xxxxxxxxxxxx", "repositoryName": "hello-container", "repositoryUri": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container", "createdAt": "2020-12-02T22:34:38+09:00", "imageTagMutability": "MUTABLE", "imageScanningConfiguration": { "scanOnPush": false }, "encryptionConfiguration": { "encryptionType": "AES256" } } }
直前でビルドしたランタイムイメージをリポジトリにプッシュします。マネジメントコンソールからリポジトリにアクセスすると、 View push commands
に以降で使うコマンドが表示されるので、これを使うと楽できてオススメです
# 手順1: ログイン $ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com Login Succeeded # 手順2: ランタイムイメージをビルド。ビルド済みの場合は飛ばしてOK $ docker image build -t "hello-container" . # 手順3: タグ付け $ docker image tag hello-container:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container:latest # 手順4: ECRにプッシュする $ docker image push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container:latest The push refers to repository [xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hello-container] 93052e3546e3: Pushed 120614c3628c: Pushed 0289476c93c4: Pushed 0bd47c7653d3: Pushed af6d16f2417e: Pushed latest: digest: sha256:db16a0eb085da33488bcce142141b1670fe9a43e40e7700b09332a237a61ff1f size: 1368
マネコンから確認します。ちゃんとプッシュされていますね!
最後にLamdbaを作って動作確認してみます。
Lambda作成&動作確認
手元のAWS CLIのバージョン(2.1.4)だとLambda作成時にコンテナイメージを指定できないようなので、マネコンから作成していきます。作成手順は以下の記事を参照してください。コンテナイメージを選択して作成するだけなのでめっちゃ楽です。
【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent
テストイベントを作成して動かしてみます。
{ "name": "Rust container image" }
ばっちり動作しました!!
まとめ
RustのカスタムランタイムがLambda Runtime APIを利用しているので簡単にコンテナ化することができました!
FFIベースのクレートに依存している場合はライブラリ導入済みのコンテナイメージを構築したり、アプリケーション動作時に参照するアセットをコンテナイメージに組み込んだり、いろんな使い方ができそうです。ぜひ色々試して便利な使い方を見つけて行きましょう!
AWS re:Invent 2020 は 12/19(JST)まで開催中です!
参加がまだの方は、この機会に是非こちらのリンクからレジストレーションして豊富なコンテンツを楽しみましょう!
AWS re:Invent | Amazon Web Services